Server side events
The /routes/sse.js file in centurion-nodejs (backend) uses server-sent events to automatically update the Centurion app (centurion-vue). /routes/sse.js is the only file within the backend that manages SSE. Other files in the backend simply call /routes/sse.js when they wish to message the frontend.
Functions
The backend functions within sse.js provide a focused message used by the frontend to update itself. Each function's name expresses its message.
ping() syncAlertsCnt(cnt) sendSyncDashboardPairs(dashboardPairsData) sendSyncHistory() sendSyncChains(chains) sendSyncFeeds(feeds) sendNewLogRecord(log)
Lastly the function send(jsonData) actually sends all messages to the frontend.
function send(jsonData) {
jsonData.ts = Date.now();
clients.forEach((client) => {
client.response.write(`data: ${JSON.stringify(jsonData)}`);
client.response.write('\n\n');
});
}
Throughout the backend any file can send one of the messages defined by the functions listed about.
const routesSse = require("../../routes/sse");
...
routesSse.sendSyncHistory();
Frontend
THe frontend (centurion-vue) receives all SSE messages in a single file /lib/sse.js. This file also starts and reconnects a connection to the backend.
Message capture
Each message from the backend has a matching capture in the event handler source.onmessage.
Each message contains a timestamp used by the handler to prevent double execution should the same message arrive more than once. This can happen if the frontend reconnects several times (user refreshes the browser many times). This condition is rare and will clear itself in a few minutes. The timestamp is stored in the store JSON object of /lib/store.js.
source.onmessage = async function (event) {
const data = JSON.parse(event.data);
...
// syncDashboard (updates the store JSON obj)
else if (data.event === "dashboardAlerts") {
if (store.sseEventIds.dashboardAlerts !== data.ts) {
store.sseEventIds.dashboardAlerts = data.ts;
store.dashboardAlerts = data.alerts;
}
}
...
// syncFeeds (emits a application wide event)
else if (data.event === "syncFeeds") {
if (store.sseEventIds.syncFeeds !== data.ts) {
store.sseEventIds.syncFeeds = data.ts;
store.feeds = data.feeds;
void emitter.emit("syncFeeds");
}
}
};
Once a message is received there is one of two actions that occur.
-
Update the reactive
storeJSON objectThese updates simply change the related value in
store. Nothing else needs to be done. Because of the reactive behavior ofstore, pages that use it simply update with no page flicker. -
Send an event to a particular page or Vue component
These update will echo an event within the frontend. Interested pages or Vue components can react as needed.
Reconnects
Reconnecting is important because computers go to sleep or are locked. When they wake up, the SSE connection will be disabled without having been closed. Then the interval shown below will check the value of store.ping which is the timestamp of the last time the server "pinged" the client. If now - store.ping > 60 seconds from the last ping, the SSE connection is closed and a new one is made.
setInterval((x) => {
const now = Date.now();
if (now - store.ping > 60000) {
source.close();
console.log('Resetting the SSE connection due to ping/ts value');
setupEventSource();
} else if (source && source.readyState === 2) {
console.log('Connection closed (network/server disconnect), will reconnect now');
setupEventSource();
}
}, 10000);
Heroku
The Heroku hosting service will drop an SSE connection unless the backend sends a message every 55 seconds or less. This is the purpose of the ping() function.
/**
* Ping clients
* Heroku has a 55 second timeout.
* This keeps the connections alive.
*/
setInterval(function () {
ping();
}, 45000);
/**
* Ping clients every 45 seconds.
*/
function ping() {
/* prettier-ignore */
const jsonData = {
"event":"ping",
};
send(jsonData);
}